﻿using System;
using System.Diagnostics;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.Serialization;

namespace SharpGL.Media3D
{
    [TypeConverter(typeof(ExpandableObjectConverter))]
    [DataContract(/*IsReference = true, */Name = "Rect3D", Namespace = "SharpGL")]
    public struct Rect3D : IFormattable
    {
        #region Private Fields

        [DataMember()]
        private float x;

        [DataMember()]
        private float y;

        [DataMember()]
        private float z;

        [DataMember()]
        private float sizeX;

        [DataMember()]
        private float sizeY;

        [DataMember()]
        private float sizeZ;

        private readonly static Rect3D s_empty = CreateEmptyRect3D();

        #endregion Private Fields
        
        #region Private Methods

        private static Rect3D CreateEmptyRect3D()
        {
            Rect3D empty = new Rect3D(); 
            // Can't use setters because they throw on negative values
            empty.x = float.PositiveInfinity;
            empty.y = float.PositiveInfinity;
            empty.z = float.PositiveInfinity;
            empty.sizeX = float.NegativeInfinity;
            empty.sizeY = float.NegativeInfinity;
            empty.sizeZ = float.NegativeInfinity;

            return empty; 
        }

        /// <summary>
        /// Helper to get the numeric list separator for a given IFormatProvider.
        /// Separator is a comma [,] if the decimal separator is not a comma, or a semicolon [;] otherwise. 
        /// </summary>
        static private char GetNumericListSeparator(IFormatProvider provider)
        {
            char numericSeparator = ',';

            // Get the NumberFormatInfo out of the provider, if possible
            // If the IFormatProvider doesn't not contain a NumberFormatInfo, then 
            // this method returns the current culture's NumberFormatInfo. 
            NumberFormatInfo numberFormat = NumberFormatInfo.GetInstance(provider);
            Debug.Assert(null != numberFormat);

            // Is the decimal separator is the same as the list separator?
            // If so, we use the ";". 
            if ((numberFormat.NumberDecimalSeparator.Length > 0) && (numericSeparator == numberFormat.NumberDecimalSeparator[0]))
            {
                numericSeparator = ';';
            }

            return numericSeparator;
        }

        /// <summary>
        /// Creates a string representation of this object based on the format string
        /// and IFormatProvider passed in. 
        /// If the provider is null, the CurrentCulture is used.
        /// See the documentation for IFormattable for more information. 
        /// </summary> 
        /// <returns>
        /// A string representation of this object. 
        /// </returns>
        internal string ConvertToString(string format, IFormatProvider provider)
        {
            if (IsEmpty)
            {
                return "Empty";
            }

            // Helper to get the numeric list separator for a given culture. 
            char separator =  GetNumericListSeparator(provider);

            return String.Format(provider,
                                 "{1:" + format + "}{0}{2:" + format + "}{0}{3:" + format + "}{0}{4:" + format + "}{0}{5:" + format + "}{0}{6:" + format + "}",
                                 separator,
                                 x,
                                 y,
                                 z,
                                 sizeX,
                                 sizeY,
                                 sizeZ);
        }
        
        #endregion Private Methods
        
        #region Constructors

        /// <summary> 
        /// Constructor which sets the position and size's initial values. Size values must be non-negative.
        /// </summary> 
        /// <param name="x">X position of the new rect.</param>
        /// <param name="y">Y position of the new rect.</param>
        /// <param name="z">Z position of the new rect.</param>
        /// <param name="sizeX">X dimension of the new rect.</param>
        /// <param name="sizeY">Y dimension of the new rect.</param>
        /// <param name="sizeZ">Z dimension of the new rect.</param>
        public Rect3D(float x, float y, float z, float sizeX, float sizeY, float sizeZ)
        {
            if (sizeX < 0 || sizeY < 0 || sizeZ < 0)
            {
                throw new System.ArgumentException("Dimension cannot be negative");
            } 
            
            this.x = x;
            this.y = y;
            this.z = z;

            this.sizeX = sizeX;
            this.sizeY = sizeY;
            this.sizeZ = sizeZ;
        }

        public Rect3D(Point3D location, Size3D size)
        {
            if (size.IsEmpty)
            {
                this = Rect3D.s_empty;
            }
            else
            {
                this.x = location.X;
                this.y = location.Y;
                this.z = location.Z;
                this.sizeX = size.X;
                this.sizeY = size.Y;
                this.sizeZ = size.Z;
            }
        }

        public Rect3D(Point3D point, Vector3D vector)
            : this(point, point + vector)
        {
        }

        public Rect3D(Point3D point1, Point3D point2)
        {
            this.x = System.Math.Min(point1.X, point2.X);
            this.y = System.Math.Min(point1.Y, point2.Y);
            this.z = System.Math.Min(point1.Z, point2.Z);

            this.sizeX = System.Math.Max(point1.X, point2.X) - this.x;
            this.sizeY = System.Math.Max(point1.Y, point2.Y) - this.y;
            this.sizeZ = System.Math.Max(point1.Z, point2.Z) - this.z;
        }

        #endregion Constructors

        #region Public Methods

        /// <summary> 
        /// Compares two Rect3D instances for object equality.  In this equality
        /// float.NaN is equal to itself, unlike in numeric equality. 
        /// Note that float values can acquire error when operated upon, such that
        /// an exact comparison between two values which
        /// are logically equal may fail.
        /// </summary> 
        /// <param name="rect1">The first Rect3D to compare.</param>
        /// <param name="rect2">The second Rect3D to compare.</param>
        /// <returns>
        /// bool - true if the two Rect3D instances are exactly equal, false otherwise 
        /// </returns> 
        public static bool Equals(Rect3D rect1, Rect3D rect2)
        {
            if (rect1.IsEmpty)
            {
                return rect2.IsEmpty;
            }
            else
            {
                return rect1.X.Equals(rect2.X) &&
                       rect1.Y.Equals(rect2.Y) &&
                       rect1.Z.Equals(rect2.Z) &&
                       rect1.SizeX.Equals(rect2.SizeX) &&
                       rect1.SizeY.Equals(rect2.SizeY) &&
                       rect1.SizeZ.Equals(rect2.SizeZ);
            }
        }

        /// <summary> 
        /// Equals - compares this Rect3D with the passed in object.  In this equality
        /// float.NaN is equal to itself, unlike in numeric equality.
        /// Note that float values can acquire error when operated upon, such that
        /// an exact comparison between two values which 
        /// are logically equal may fail.
        /// </summary> 
        /// <param name="o">The object to compare to "this".</param>
        /// <returns> 
        /// bool - true if the object is an instance of Rect3D and if it's equal to "this".
        /// </returns> 
        public override bool Equals(object o)
        {
            if ((null == o) || !(o is Rect3D))
            {
                return false;
            }

            Rect3D value = (Rect3D)o;
            return Rect3D.Equals(this, value);
        }

        /// <summary> 
        /// Equals - compares this Rect3D with the passed in object.  In this equality
        /// float.NaN is equal to itself, unlike in numeric equality. 
        /// Note that float values can acquire error when operated upon, such that 
        /// an exact comparison between two values which
        /// are logically equal may fail. 
        /// </summary>
        /// <param name="value">The Rect3D to compare to "this".</param>
        /// <returns>
        /// bool - true if "value" is equal to "this".
        /// </returns> 
        public bool Equals(Rect3D value)
        {
            return Rect3D.Equals(this, value);
        }

        public bool Contains(Point3D point)
        {
            return Contains(point.X, point.Y, point.Z);
        }

        public bool Contains(Rect3D rect)
        {
            if (IsEmpty)
            {
                return false;
            }

            return (rect.X >= this.X) &&
                   (rect.Y >= this.Y) &&
                   (rect.Z >= this.Z) &&
                   (rect.X + rect.SizeX <= this.X + this.SizeX) &&
                   (rect.Y + rect.SizeY <= this.Y + this.SizeY) &&
                   (rect.Z + rect.SizeZ <= this.Z + this.SizeZ);
        }

        public bool Contains(float x, float y, float z)
        {
            if (IsEmpty)
            {
                return false;
            }

            return (x >= this.X) &&
                   (x <= (this.X + this.SizeX)) &&
                   (y >= this.Y) &&
                   (y <= (this.Y + this.SizeY)) &&
                   (z >= this.Z) &&
                   (z <= (this.Z + this.SizeZ));
        }

        public void Intersect(Rect3D rect)
        {
            if (IsEmpty)
            {
                this = rect;
            }
            else
            {
                float minX = Math.Max(this.X, rect.X);
                float minY = Math.Max(this.Y, rect.Y);
                float minZ = Math.Max(this.Z, rect.Z);

                this.SizeX = Math.Min(this.X + this.SizeX, rect.X + rect.SizeX) - minX;
                this.SizeY = Math.Min(this.Y + this.SizeY, rect.Y + rect.SizeY) - minY;
                this.SizeZ = Math.Min(this.Z + this.SizeZ, rect.Z + rect.SizeZ) - minZ;
                this.X = minX;
                this.Y = minY;
                this.Z = minZ;
            }
        }

        public static Rect3D Intersect(Rect3D rect1, Rect3D rect2)
        {
            rect1.Intersect(rect2);
            return rect1;
        }
        
        public void Union(Point3D point)
        {
            Union(new Rect3D(point, point));
        }

        public void Union(Rect3D rect)
        {
            if (IsEmpty)
            {
                this = rect;
            }
            else
            {
                float minX = Math.Min(this.X, rect.X);
                float minY = Math.Min(this.Y, rect.Y);
                float minZ = Math.Min(this.Z, rect.Z);

                this.SizeX = Math.Max(this.X + this.SizeX, rect.X + rect.SizeX) - minX;
                this.SizeY = Math.Max(this.Y + this.SizeY, rect.Y + rect.SizeY) - minY;
                this.SizeZ = Math.Max(this.Z + this.SizeZ, rect.Z + rect.SizeZ) - minZ;
                this.X = minX;
                this.Y = minY;
                this.Z = minZ;
            }
        }

        public static Rect3D Union(Rect3D rect, Point3D point)
        {
            rect.Union(point);
            return rect;
        }

        public static Rect3D Union(Rect3D rect1, Rect3D rect2)
        {
            rect1.Union(rect2);
            return rect1;
        }

        /// <summary>
        /// Returns the HashCode for this Rect3D.
        /// </summary>
        /// <returns> 
        /// int - the HashCode for this Rect3D.
        /// </returns> 
        public override int GetHashCode()
        {
            if (IsEmpty)
            {
                return 0;
            }
            else
            {
                // Perform field-by-field XOR of HashCodes 
                return X.GetHashCode() ^
                       Y.GetHashCode() ^
                       Z.GetHashCode() ^
                       SizeX.GetHashCode() ^
                       SizeY.GetHashCode() ^
                       SizeZ.GetHashCode();
            }
        }

        /// <summary> 
        /// Creates a string representation of this object based on the current culture.
        /// </summary> 
        /// <returns> 
        /// A string representation of this object.
        /// </returns> 
        public override string ToString()
        {
            // Delegate to the internal method which implements all ToString calls. 
            return ConvertToString(null /* format string */, null /* format provider */);
        }

        /// <summary>
        /// Creates a string representation of this object based on the IFormatProvider 
        /// passed in.  If the provider is null, the CurrentCulture is used.
        /// </summary>
        /// <returns>
        /// A string representation of this object. 
        /// </returns>
        public string ToString(IFormatProvider provider)
        {
            // Delegate to the internal method which implements all ToString calls. 
            return ConvertToString(null /* format string */, provider);
        }

        /// <summary> 
        /// Creates a string representation of this object based on the format string
        /// and IFormatProvider passed in. 
        /// If the provider is null, the CurrentCulture is used. 
        /// See the documentation for IFormattable for more information.
        /// </summary> 
        /// <returns>
        /// A string representation of this object.
        /// </returns>
        string IFormattable.ToString(string format, IFormatProvider provider)
        {
            // Delegate to the internal method which implements all ToString calls. 
            return ConvertToString(format, provider);

        }

        #endregion Public Methods

        #region Public Operators

        /// <summary> 
        /// Compares two Rect3D instances for exact equality.
        /// Note that double values can acquire error when operated upon, such that 
        /// an exact comparison between two values which are logically equal may fail.
        /// Furthermore, using this equality operator, Double.NaN is not equal to itself.
        /// </summary>
        /// <param name="rect1">The first Rect3D to compare.</param>
        /// <param name="rect2">The second Rect3D to compare.</param>
        /// <returns> 
        /// bool - true if the two Rect3D instances are exactly equal, false otherwise.
        /// </returns> 
        public static bool operator ==(Rect3D rect1, Rect3D rect2)
        {
            return rect1.X == rect2.X &&
                   rect1.Y == rect2.Y &&
                   rect1.Z == rect2.Z &&
                   rect1.SizeX == rect2.SizeX &&
                   rect1.SizeY == rect2.SizeY &&
                   rect1.SizeZ == rect2.SizeZ;
        }

        /// <summary>
        /// Compares two Rect3D instances for exact inequality.
        /// Note that double values can acquire error when operated upon, such that
        /// an exact comparison between two values which are logically equal may fail. 
        /// Furthermore, using this equality operator, Double.NaN is not equal to itself.
        /// </summary> 
        /// <returns> 
        /// bool - true if the two Rect3D instances are exactly unequal, false otherwise
        /// </returns> 
        /// <param name="rect1">The first Rect3D to compare
        /// <param name="rect2">The second Rect3D to compare
        public static bool operator !=(Rect3D rect1, Rect3D rect2)
        {
            return !(rect1 == rect2);
        }

        #endregion Public Operators

        #region Public Properties

        /// <summary>
        /// Empty - a static property which provides an Empty rect.
        /// SizeX, SizeY, and SizeZ are negative-infinity.
        /// This is the only situation where size can be negative. 
        /// </summary> 
        public static Rect3D Empty
        {
            get
            {
                return s_empty;
            }
        }

        /// <summary>
        /// IsEmpty - this returns true if this rect is the Empty rect.
        /// Note: If size is 0 this Rect3D still contains a 0, 1, or 2 dimensional set 
        /// of points, so this method should not be used to check for 0 volume.
        /// </summary> 
        public bool IsEmpty
        {
            get
            {
                return sizeX < 0;
            }
        }

        /// <summary> 
        /// Gets or sets the area of the Rect3D. 
        /// </summary> 
        public Size3D Size
        {
            get
            {
                return new Size3D(sizeX, sizeY, sizeZ);
            }
            set
            {
                if (IsEmpty)
                {
                    throw new System.InvalidOperationException("Cannot modify empty rect");
                }

                if (value.IsEmpty)
                {
                    throw new System.ArgumentException("Cannot set empty rect size");
                }

                sizeX = value.X;
                sizeY = value.Y;
                sizeZ = value.Z;
            }
        }
        
        /// <summary> 
        /// Gets or sets the size of the Rect3D in the X dimension.
        /// Default is 0, must be non-negative. 
        /// </summary>
        [Browsable(false)]
        public float SizeX 
        {
            get
            {
                return sizeX; 
            }
            set
            { 
                if (IsEmpty)
                { 
                    throw new System.InvalidOperationException("Cannot modify empty rect");
                }

                if (value < 0) 
                {
                    throw new System.ArgumentException("Dimension cannot be negative"); 
                } 

                sizeX = value; 
            }
        }

        /// <summary> 
        /// Gets or sets the size of the Rect3D in the Y dimension.
        /// Default is 0, must be non-negative. 
        /// </summary>
        [Browsable(false)]
        public float SizeY
        {
            get
            {
                return sizeY;
            }
            set
            {
                if (IsEmpty)
                {
                    throw new System.InvalidOperationException("Cannot modify empty rect");
                }

                if (value < 0)
                {
                    throw new System.ArgumentException("Dimension cannot be negative");
                }

                sizeY = value;
            }
        }

        /// <summary> 
        /// Gets or sets the size of the Rect3D in the Z dimension.
        /// Default is 0, must be non-negative. 
        /// </summary>
        [Browsable(false)]
        public float SizeZ
        {
            get
            {
                return sizeZ;
            }
            set
            {
                if (IsEmpty)
                {
                    throw new System.InvalidOperationException("Cannot modify empty rect");
                }

                if (value < 0)
                {
                    throw new System.ArgumentException("Dimension cannot be negative");
                }

                sizeZ = value;
            }
        }

        /// <summary> 
        /// Gets or sets a Point3D that represents the origin of the Rect3D.
        /// </summary> 
        public Point3D Location
        {
            get { return new Point3D(X, Y, Z); }
            set
            {
                if (IsEmpty)
                {
                    throw new InvalidOperationException("Cannot set the location of an empty Rect3D");
                }

                this.X = value.X;
                this.Y = value.Y;
                this.Z = value.Z;
            }
        }

        /// <summary> 
        /// Gets or sets the value of the X coordinate of the Rect3D. 
        /// </summary> 
        [Browsable(false)]
        public float X 
        {
            get
            {
                return x;
            }
            set
            {
                if (IsEmpty) 
                { 
                    throw new System.InvalidOperationException("Cannot modify empty rect");
                } 

                x = value; 
            }
        }

        /// <summary> 
        /// Gets or sets the value of the Y coordinate of the Rect3D. 
        /// </summary> 
        [Browsable(false)]
        public float Y
        {
            get
            {
                return y;
            }
            set
            {
                if (IsEmpty)
                {
                    throw new System.InvalidOperationException("Cannot modify empty rect");
                }

                y = value;
            }
        }

        /// <summary> 
        /// Gets or sets the value of the Z coordinate of the Rect3D. 
        /// </summary> 
        [Browsable(false)]
        public float Z
        {
            get
            {
                return z;
            }
            set
            {
                if (IsEmpty)
                {
                    throw new System.InvalidOperationException("Cannot modify empty rect");
                }

                z = value;
            }
        }

        #endregion Public Properties
    }
}
